#!/usr/bin/env python
# -*- coding: utf-8 -*-#
# @(#)test_runner.py.in
#
#
# Copyright (C) 2013, GC3, University of Zurich. All rights reserved.
#
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2 of the License, or (at your
# option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""
This script will create one test for each directory containing a
``geotop.inpts`` file, will run geotop inside that directory and will
check the output files against the files contained in the ``results``
subdirectory.

Specifically, each test run on a TEST_DIR subdirectory will pass if
and only if:

* TEST_DIR/_SUCCESSFUL_RUN exists and is a file

* for each OUTPUTDIR in TEST_DIR/results:
    for each FILE in OUTPUTDIR:
      - TEST_DIR/OUTPUTDIR/FILE exists and is a file
      - TEST_DIR/OUTPUTDIR/FILE is equal to TEST_DIR/results/OUTPUTDIR/FILE

Adding a new test should be as easy as:

* adding a directory with input files for geotop

* adding a subdirectory ``results`` containing the correct output
  files generated by geotop.

"""

__author__ = 'Antonio Messina <antonio.s.messina@gmail.com>'
__docformat__ = 'reStructuredText'

import filecmp
import os
import re
import subprocess
import shutil
import sys
import tempfile

try:
    import pandas
except ImportError:
    pandas = None

if pandas:
    import warnings
    warnings.simplefilter(action = "ignore", category = FutureWarning)

from nose.tools import assert_equal, assert_true
from nose.plugins.skip import SkipTest

def assert_is_file(fname, msg=None):
    assert os.path.isfile(fname), msg or "%s is not a file" % fname

# Directory containing this file
TESTDIR = "@abs_top_builddir@/tests/test_sample_run"

# Path to the geotop binary
GEOTOP = "@abs_top_builddir@/@GEOTOP_PROGRAM_PATH@"
RESULTDIR = "@METEODISTR_RESULTS_DIR@"

def compare_files_exactly(fpath_ok, fpath_new, delta):
    """
    Compare the *content* of two files.

    Current implementation only check that they are *exactly* the
    same files. Future implementations will also allow a numeric
    tolerance on the values, if needed.
    """
    assert_true(filecmp.cmp(fpath_ok, fpath_new), msg="File %s and %s differ!" % (fpath_ok, fpath_new))

def compare_tab_files(fpath_ok, fpath_new, deltas):
    """
    Compare the data in two files and check if they are *almost*
    equal, depending on a delta which depends on the field name.
    """
    data_ok = pandas.read_csv(fpath_ok, na_values=['nan', '-nan']).fillna(0)
    data_new = pandas.read_csv(fpath_new, na_values=['nan', '-nan']).fillna(0)
    # get the correct delta value/dictionary

    # check fields one by one
    for field in data_ok:
        if field in deltas:
            assert_true(
                (abs(data_ok[field] - data_new[field]) <= deltas[field]).all(),
                msg="Field '%s' in file %s is too different: %d rows differ "
                "more than %f from the correct ones (maxdiff=%f)." % (
                    field, fpath_new, (abs(data_ok[field] - data_new[field]) > deltas[field]).sum(),
                    deltas[field], abs(data_ok[field] - data_new[field]).max()))
        else:
            # Check that all the fields are equal
            assert_true((data_ok[field] == data_new[field]).all(),
                        msg="Field '%s' of files %s and %s differs!" % (field, fpath_ok, fpath_new))

def compare_map_files(fpath_ok, fpath_new, deltas):
    """
    Compare the data in two files and check if they are *almost*
    equal, depending on a delta which depends on the field name.
    """
    data_ok = pandas.read_csv(fpath_ok, sep=' ', skiprows=6, header=None, na_values=['nan', '-nan'])
    data_new = pandas.read_csv(fpath_new, sep=' ', skiprows=6, header=None, na_values=['nan', '-nan'])
    assert_equal(data_ok.shape, data_new.shape)
    # get the correct delta value/dictionary
    try:
        diff = (data_ok - data_new).apply(abs).fillna(0)
    except (ValueError, TypeError):
        return compare_files_exactly(fpath_ok, fpath_new, None)
    assert_true(
        (diff <= deltas).all().all(),
        msg="File %s: %d fields are bigger than predefined delta: "
        "%f > %f" % (fpath_new, (diff > deltas).count().sum(), diff.max().max(), deltas)
    )

class TestValidRun(object):
    cmp_functions = [
        (r'(.*/)?point_info_[0-9]{4}.txt', compare_files_exactly, None),
        (r'(.*/)?.*[0-9]{4}.txt', compare_tab_files, {
            'snow_depth[mm]'                  : 3.0,
            'snow_water_equivalent[mm]'       : 1.0,
            'Tair[C]'                         : 0.0,
            'Relative_Humidity[-]'            : 0.0,
            'Tsurface[C]'                     : 3.0,
            'Surface_Energy_balance[W/m2]'   : 20.0,
            'Soil_heat_flux[W/m2]'           : 50.0,
            'SWin[W/m2]'                      : 0.6,
            'SWbeam[W/m2]'                    : 0.0,
            'SWdiff[W/m2]'                    : 0.6,
            'LWnet[W/m2]'                    : 15.0,
            'LWin[W/m2]'                      : 0.0,
            'SWnet[W/m2]'                    : 20.0,
            'Run'                             : 0.0,
            '140.000000 '                     : 0.1,
            '530.000000 '                     : 0.1,
            '1780.000000  '                   : 0.1,
            }),
        # (r'(.*/)?SWEN.*.asc', compare_map_files, 1.0),
        (r'(.*/)?SWEDENSITYN.*.asc', compare_map_files, 100.0),
        # (r'(.*/)?MMGSTN.*.asc', compare_map_files, 7.4),
        # (r'(.*/)?TLLLLLN.*.asc', compare_map_files, 0.3),
        (r'(.*/)?snowdepthN.*.asc', compare_map_files, 10.0),
        (r'(.*/)?.*.asc', compare_map_files, 1.0),
    ]

    def setUp(self):
        os.chdir(TESTDIR)

    def compare_files(self, fpath_ok, fpath_new):
        """
        Call self.compare_files_using_delta() if we do know the format
        of the file, and self.compare_files_exactly if fpath_ok otherwise.
        """
        if not pandas:
            return compare_files_exactly(fpath_ok, fpath_new, None)

        for (regexp, func, delta) in self.cmp_functions:
            if re.match(regexp, fpath_ok):
                return func(fpath_ok, fpath_new, delta)
        return compare_files_exactly(fpath_ok, fpath_new, None)

    def _test_template(self, directory):
        """
        This template function will run geotop on a specific test
        directory, check if the `_SUCCESSFUL_RUN` is created, and
        check that all the files stored in ``results`` have been
        created by the simulation and compare them.
        """
        if os.path.isfile(os.path.join(directory, 'DISABLE_TEST_RUNNER')):
            raise SkipTest(
                "Skipping test `%s` because 'DISABLE_TEST_RUNNER' file is "
                "present" % os.path.basename(directory))
        # Change the current working directory, because
        # ``geotop.inpts`` files usually have relative paths.
        os.chdir(directory)

        assert_is_file(GEOTOP)

        # Run geotop
        command = subprocess.Popen(
            [GEOTOP, '.'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        (stdout, stderr) = command.communicate()
        assert_equal(command.returncode, 0, msg=stderr)

        # Check that _SUCCESSFUL_RUN file exists)
        assert_is_file(os.path.join(directory, '_SUCCESSFUL_RUN'))

        resultdir = os.path.join(directory, RESULTDIR)
        if not os.path.isdir(resultdir):
            # No `results` directory. Assume it's OK.
            raise SkipTest("Skipping test because no resultdir `%s` is "
                           "present" % resultdir)

        # For each directory inside ``results``...
        for outputdir in os.listdir(resultdir):
            # and for each file in this directory...
            for fname in os.listdir(os.path.join(resultdir, outputdir)):
                # fpath_ok is the file in ``results``
                fpath_ok = os.path.join(resultdir, outputdir, fname)
                # fpath_new is the corresponding file just produced by geotop
                fpath_new = os.path.join(directory, outputdir, fname)
                # fpath_new has to exists and...
                if not os.path.isfile(fpath_new): continue
                assert_is_file(fpath_new)
                # ...it has to be equal to the file in ``results``
                self.compare_files(fpath_ok, fpath_new)

    def test_generator(self):
        for d in os.listdir(TESTDIR):
            path = os.path.join(TESTDIR, d)
            if os.path.isdir(path) and \
              os.path.isfile(os.path.join(path, 'geotop.inpts')):
                yield self._test_template, path

if __name__ == "__main__":
    import nose
    nose.run()
